package server.net;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import server.GameServer;
import server.net.io.Dispatcher;
import server.net.io.ServerIOHandler;

/**
 * This class encapsulates a client's socket channel and dispatcher.
 * We should only be using this class for low-level network IO things.
 * Any higher game logic belongs in the NimClient object.
 *
 * @author Vincent Tsuei
 * @version 11-13-2012
 */
public class ClientConnection {
    
    
    // Low level IO-related fields
    private Dispatcher dispatcher;
    private SocketChannel socket;
    private Client client;
    
    // Processing Packets
    private ByteBuffer remaining;
    
    // Write Handling. Queue and write buffer.
    private ConcurrentLinkedQueue<byte[]> write = new ConcurrentLinkedQueue<byte[]>();
    private static final int INITIAL_WRITE_BUFFER_SIZE = 4096;
    private static final ThreadLocal<ByteBuffer> bufferStore = new ThreadLocal<ByteBuffer>() {
        @Override
        protected ByteBuffer initialValue() {
            return ByteBuffer.allocate(INITIAL_WRITE_BUFFER_SIZE);
        }
    };
    
    // IP/Port string cache
    private String ipportstr = null;
            
    public ClientConnection(GameServer server, SocketChannel clientSocket){
        this.socket = clientSocket;
        this.client = new Client(server, this); //Server ref belongs in NimClient, higher logic
    }
    
    /**
     * If there is any partial packet, save them for later.
     */
    public void setRemainingBuffer(ByteBuffer b) {
        remaining = b;
    }
    
    /**
     * Lets us know if there was a partial packet not yet processed.
     */
    public boolean hasRemainingBuffer() {
        return remaining != null;
    }
    
    /**
     * Get the partial packet, if any exists. Returns null if there was none.
     */
    public ByteBuffer getRemainingBuffer() {
        if (hasRemainingBuffer()) {
            return remaining;
        } else {
            return null; //Throw RuntimeException?
        }
    }
    
    /**
     * Clear any partial packet.
     */
    public void clearRemainingBuffer() {
        remaining = null;
    }
    
    /**
     * Shutdown this NimConnection.
     * We do this by deregistering from the dispatcher, then closing the socket.
     */
    public void shutdown() {
        this.dispatcher.deregister(this);
        try {
            this.socket.close();
        } catch (IOException ex) {
            Logger.getLogger(ClientConnection.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    /**
     * Get the NimClient associated with this connection.
     */
    public Client getClient() {
        return client;
    }
    
    /**
     * Get the SocketChannel associated with this connection.
     */
    public SocketChannel getSocketChannel() {
        return socket;
    }
    
    /**
     * Attach a dispatcher to this NimConnection. Only works once.
     */
    public void linkDispatcher(Dispatcher d) {
        if (dispatcher == null){
            dispatcher = d;
        } else {
            throw new RuntimeException("Trying to overwrite a Dispatcher link");
        }
    }
    
    /**
     * Send this packet along this connection.
     */
    public void queuePacketSend(byte[] b) {
        write.offer(b);
        dispatcher.notifyWriteNeeded(this);
    }
    
    /**
     * Signal the connection to handle any pending writes.
     */
    public void doWrite() {
        byte[] nextPacket = write.poll();
        ByteBuffer writeBuffer = bufferStore.get();
        boolean empty = true;
        while (nextPacket != null) {
            empty = false;
            writeBuffer.putInt(nextPacket.length);
            writeBuffer.put(nextPacket);
            nextPacket = write.poll();
            if (nextPacket != null && nextPacket.length + 4 > writeBuffer.remaining()) {
                writeAndClearBuffer(writeBuffer, socket);
                empty = true; //Not really needed, but just in case.
            }
        }
        if (!empty) {
            writeAndClearBuffer(writeBuffer, socket);
        }
    }
    
    /**
     * This function actually does the writing.
     * First it flips the buffer, then writes it out, then clears it for
     * further use.
     */
    private void writeAndClearBuffer(ByteBuffer b, SocketChannel chan) {
        b.flip();
        try {
            chan.write(b);
            b.clear();
        } catch (IOException ex) {
            Logger.getLogger(ServerIOHandler.class.getName()).log(Level.SEVERE, null, ex); //Major error!
        }
    }
    
    /**
     * Return the address and port of the client.
     */
    public String getAddressAndPortString() {
        // We don't care about concurrency here, so no synchronization.
        // Worst case, we will create and assign this string multiple times.
        if (ipportstr != null) {
            return ipportstr;
        }
        ipportstr = socket.socket().getInetAddress() + ":" + Integer.toString(socket.socket().getPort());
        return ipportstr;
    }
}
